﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using Felbot.Interaction;

namespace Felbot.Scripting {
    /// <summary>
    /// This class loads .NET scripts and calls functions.
    /// </summary>
    public static class ScriptControl {
        private static Dictionary<string, Assembly> m_Scripts = new Dictionary<string, Assembly>();
        private static Dictionary<string, string[]> m_scriptinfo = new Dictionary<string, string[]>();

        public static int ScriptCount {
            get {
                return m_Scripts.Count;
            }
        }

        public static List<ScriptInfo> GetScriptList() {
            List<ScriptInfo> scriptList = new List<ScriptInfo>();
            foreach (KeyValuePair<string, string[]> kvp in m_scriptinfo) {
                scriptList.Add(new ScriptInfo(kvp));
            }
            return scriptList;
        }

        public static ScriptInfo[] LoadScripts(DirectoryInfo dir, string filter) {
            FileInfo[] files = dir.GetFiles(filter, SearchOption.AllDirectories);
            ScriptInfo[] scriptInfo = new ScriptInfo[files.Length];
            for (int i = 0; i < files.Length; i++) {
                scriptInfo[i] = LoadScript(files[i]);
            }
            return scriptInfo;
        }

        public static ScriptInfo LoadScript(FileInfo scriptFile) {
            if (!scriptFile.Exists)
                throw new FileNotFoundException("Script file not found.", scriptFile.FullName);

            CodeDomProvider codeProvider;
            CompilerParameters compParams;
            CompilerResults compResults;
            string[] scriptLines = File.ReadAllLines(scriptFile.FullName);
            string scriptName;
            string scriptInternalName = null;
            string scriptLanguage = "C#";
            string scriptVersion = "v0.0";
            double scriptVersionValue = 0.0;
            string scriptAuthor = null;
            string scriptDescription = null;
            List<string> scriptNotes = new List<string>();
            string scriptNotesString = null;
            string scriptPrefix;
            string scriptSuffix;
            string scriptContent;
            string line;
            KeyValuePair<string, string[]> scriptInfoKvp;
            ScriptInfo scriptInfo = null;

            scriptName = scriptFile.Name.Split(".".ToCharArray(), 2)[0];
            int i = 0;
            int j = 0;
            if (scriptLines.Length == 0)
                throw new Exception("Script file empty.");
            while (scriptLines[i].Trim().Length == 0 ||
                   (scriptLines[i].Trim().Length > 2 &&
                    (scriptLines[i].Trim().StartsWith("//") || scriptLines[i].Trim().StartsWith("''")))) {
                if (scriptLines[i].Trim().Length == 0) {
                    i++;
                    continue;
                }
                line = scriptLines[i].Trim().Substring(2).Trim();
                switch (j) {
                    case 0: //name
                        scriptName = line;
                        break;
                    case 1: //language
                        scriptLanguage = line;
                        break;
                    case 2: //version
                        scriptVersion = line;
                        if (!double.TryParse(scriptVersion, out scriptVersionValue))
                            if (!double.TryParse(scriptVersion.Substring(1).Trim(), out scriptVersionValue))
                                scriptVersionValue = 0.0;
                        break;
                    case 3: //author
                        scriptAuthor = line;
                        break;
                    case 4: //description
                        scriptDescription = line;
                        break;
                    default: //notes
                        scriptNotes.Add(line);
                        break;
                }
                i++;
                j++;
                if (scriptLines.Length < i)
                    throw new Exception("Script file contains no code.");
            }

            scriptInternalName = Regex.Replace(scriptName.ToLower(), "(^[^a-z])|([^a-z0-9_])", "");
            if (m_Scripts.ContainsKey(scriptInternalName))
                throw new Exception("Script by the name \"" + scriptInternalName + "\" already loaded.");

            if (scriptNotes.Count > 0)
                foreach (string scriptNote in scriptNotes)
                    scriptNotesString += scriptNote + Environment.NewLine;

            switch (scriptLanguage.ToLower()) {
                default:
                case "c#":
                case "csharp":
                case "c sharp":
                    scriptLanguage = "CSharp";
                    scriptPrefix =
                                 "using System;" + Environment.NewLine +
                                 "using System.Text;" + Environment.NewLine +
                                 "using System.Collections;" + Environment.NewLine +
                                 "using System.Collections.Generic;" + Environment.NewLine +
                                 "using Felbot.Scripting.ScriptSupport;" + Environment.NewLine +
                                 Environment.NewLine +
                                 "namespace Felbot.Scripting {" + Environment.NewLine +
                                 "    public class " + scriptInternalName + " {" + Environment.NewLine;
                    scriptSuffix =
                                 Environment.NewLine +
                                 "    }" + Environment.NewLine +
                                 "}";
                    break;

                case "js":
                case "jscript":
                case "javascript":
                case "java script":
                    scriptLanguage = "JScript";
                    scriptPrefix =
                                 "import System;" + Environment.NewLine +
                                 "import System.Text;" + Environment.NewLine +
                                 "import System.Collections;" + Environment.NewLine +
                                 "import System.Collections.Generic;" + Environment.NewLine +
                                 "import Felbot.Scripting.ScriptSupport;" + Environment.NewLine +
                                 Environment.NewLine +
                                 "namespace Felbot.Scripting {" + Environment.NewLine +
                                 "    public class " + scriptInternalName + " {" + Environment.NewLine;
                    scriptSuffix =
                                 Environment.NewLine +
                                 "    }" + Environment.NewLine +
                                 "}";
                    break;

                case "f#":
                case "fsharp":
                case "f sharp":
                    scriptLanguage = "FSharp";
                    scriptPrefix =
                                 "#light" + Environment.NewLine +
                                 "namespace Felbot.Scripting" + Environment.NewLine +
                                 "open System" + Environment.NewLine +
                                 "open System.Text" + Environment.NewLine +
                                 "open System.Collections" + Environment.NewLine +
                                 "open System.Collections.Generic" + Environment.NewLine +
                                 "open Felbot.Scripting.ScriptSupport" + Environment.NewLine +
                                 "type " + scriptInternalName + " = class" + Environment.NewLine;
                    scriptSuffix =
                                 "    end";
                    break;

                case "vb":
                case "visualbasic":
                case "visual basic":
                case "vbnet":
                case "vb net":
                case "vb.net":
                case "vbdotnet":
                case "visualbasicnet":
                case "visualbasic net":
                case "visualbasic.net":
                case "visualbasicdotnet":
                case "visual basic net":
                case "visual basic.net":
                case "visual basicdotnet":
                case "visual basic dot net":
                    scriptLanguage = "VisualBasic";
                    scriptPrefix =
                                 "Imports System" + Environment.NewLine +
                                 "Imports System.Text" + Environment.NewLine +
                                 "Imports System.Collections" + Environment.NewLine +
                                 "Imports System.Collections.Generic" + Environment.NewLine +
                                 "Imports Felbot.Scripting.ScriptSupport" + Environment.NewLine +
                                 Environment.NewLine +
                                 "Namespace Felbot.Scripting" + Environment.NewLine +
                                 "    Public Class " + scriptInternalName + Environment.NewLine;
                    scriptSuffix =
                                 Environment.NewLine +
                                 "    End Class" + Environment.NewLine +
                                 "End Namespace";
                    break;
            }
            try {
                codeProvider = CodeDomProvider.CreateProvider(scriptLanguage);
            } catch (ConfigurationException) {
                throw new Exception("The language " + scriptLanguage + " is supported by Felbot but it is not installed on the system.");
            }

            compParams = new CompilerParameters();
            compParams.GenerateExecutable = false;
            compParams.GenerateInMemory = true;
            compParams.IncludeDebugInformation = false;
            compParams.ReferencedAssemblies.Add("mscorlib.dll");
            compParams.ReferencedAssemblies.Add("System.dll");
            compParams.ReferencedAssemblies.Add(Application.ExecutablePath);

            scriptContent = scriptPrefix + File.ReadAllText(scriptFile.FullName) + scriptSuffix;
            string scriptContentRevised;
            foreach (string contentLine in scriptContent.Split(Environment.NewLine[0])) {
                scriptContentRevised = "        " + contentLine.TrimStart(Environment.NewLine.ToCharArray());
            }
            compResults = codeProvider.CompileAssemblyFromSource(compParams, scriptContent);

            if (compResults.NativeCompilerReturnValue == 0)
                m_Scripts.Add(scriptInternalName, compResults.CompiledAssembly);
            scriptInfoKvp = new KeyValuePair<string, string[]>(
                scriptInternalName,
                new string[]
                        {
                            scriptName,
                            scriptFile.FullName,
                            scriptLanguage,
                            scriptVersionValue.ToString(),
                            scriptAuthor,
                            scriptDescription,
                            scriptNotesString
                        }
                );
            m_scriptinfo.Add(scriptInfoKvp.Key, scriptInfoKvp.Value);
            scriptInfo = new ScriptInfo(scriptInfoKvp, compResults, scriptLines);
            return scriptInfo;
        }

        public static void UnloadScripts() {
            m_Scripts.Clear();
            m_scriptinfo.Clear();
        }

        public static void UnloadScript(string scriptName) {
            if (m_Scripts.ContainsKey(scriptName)) {
                m_Scripts.Remove(scriptName);
                m_scriptinfo.Remove(scriptName);
            }
        }

        public static void ReloadScripts() {
            string[] scripts = new string[m_Scripts.Count];
            m_Scripts.Keys.CopyTo(scripts, 0);
            for (int i = 0; i < scripts.Length; i++) {
                ReloadScript(scripts[i]);
            }
        }

        public static void ReloadScript(string scriptName) {
            if (m_Scripts.ContainsKey(scriptName)) {
                string filepath = m_scriptinfo[scriptName][1];
                UnloadScript(scriptName);
                LoadScript(new FileInfo(filepath));
            }
        }

        public static ScriptFunctionReturn[] CallFunction(CommandSource source, string functionName) {
            return CallFunction(source, functionName, new object[0]);
        }

        public static ScriptFunctionReturn[] CallFunction(CommandSource source, string functionName, params object[] parameters) {
            ScriptFunctionReturn[] scriptReturn = new ScriptFunctionReturn[m_Scripts.Count];
            int i = 0;
            foreach (string scriptName in m_Scripts.Keys) {
                scriptReturn[i] = CallFunction(source, scriptName, functionName, parameters);
                i++;
            }
            return scriptReturn;
        }

        public static ScriptFunctionReturn CallFunction(CommandSource source, string scriptName, string functionName) {
            return CallFunction(source, scriptName, functionName, new object[0]);
        }

        public static ScriptFunctionReturn CallFunction(CommandSource source, string scriptName, string functionName, params object[] parameters) {
            if (!m_Scripts.ContainsKey(scriptName.ToLower()))
                return new ScriptFunctionReturn(ScriptFunctionReturnType.DoesNotExist, null, null, scriptName, functionName);

            Assembly assm = m_Scripts[scriptName.ToLower()];
            ScriptEventArgs seargs = new ScriptEventArgs(new ScriptData(source, parameters));

            foreach (Type type in assm.GetModules(false)[0].GetTypes()) {
                foreach (MethodInfo method in type.GetMethods(BindingFlags.Static | BindingFlags.Public)) {
                    if (method.Name.Equals(functionName, StringComparison.CurrentCultureIgnoreCase)) {
                        try {
                            if (method.ReturnType.Equals(typeof(void))) {
                                method.Invoke(null, new object[] { seargs });
                                return new ScriptFunctionReturn(ScriptFunctionReturnType.ReturnsVoid, null, null, scriptName, functionName);
                            } else {
                                return new ScriptFunctionReturn(ScriptFunctionReturnType.ReturnsObject, method.Invoke(null, new object[] { seargs }), null, scriptName, functionName);
                            }
                        } catch (Exception e) {
                            return new ScriptFunctionReturn(ScriptFunctionReturnType.ExecutionException, null, new ScriptException("A script has encountered an exception.", e), scriptName, functionName);
                        }
                    }
                }
            }
            return new ScriptFunctionReturn(ScriptFunctionReturnType.DoesNotExist, null, null, scriptName, functionName);
        }
    }

    /// <summary>
    /// This represents the results of a script function call.
    /// </summary>
    public class ScriptFunctionReturn {
        private ScriptFunctionReturnType m_ReturnType;
        private object m_ReturnValue;
        private ScriptException m_Exception;
        private string m_ScriptName;
        private string m_FunctionName;

        /// <summary>
        /// The return type of the function.
        /// </summary>
        /// <remarks>
        /// Check this to make sure the correct data is required,
        /// or else you might get a NullReferenceException trying to access "ReturnValue".
        /// </remarks>
        public ScriptFunctionReturnType ReturnType {
            get {
                return m_ReturnType;
            }
        }
        /// <summary>
        /// The return value from the function call. Null if nothing returned.
        /// </summary>
        public object ReturnValue {
            get {
                return m_ReturnValue;
            }
        }

        /// <summary>
        /// An exception that occured. Null if none occured.
        /// </summary>
        public ScriptException Exception {
            get {
                return m_Exception;
            }
        }

        /// <summary>
        /// The name of the script containing function.
        /// </summary>
        public string ScriptName {
            get {
                return m_ScriptName;
            }
        }

        /// <summary>
        /// The name of the function called.
        /// </summary>
        public string FunctionName {
            get {
                return m_FunctionName;
            }
        }

        /// <summary>
        /// Initializes a new ScriptFunctionReturn object.
        /// </summary>
        /// <param name="type">The return type.</param>
        /// <param name="ret">The return value.</param>
        /// <param name="excs">A list of exceptions.</param>
        /// <param name="scriptName">The name of the script containing function.</param>
        /// <param name="functionName">The name of the function called.</param>
        internal ScriptFunctionReturn(ScriptFunctionReturnType type, object ret, ScriptException excs,
                                      string scriptName, string functionName) {
            m_ReturnType = type;
            m_ReturnValue = ret;
            m_Exception = excs;
            m_ScriptName = scriptName;
            m_FunctionName = functionName;
        }
    }

    /// <summary>
    /// Stores information about a loaded script.
    /// </summary>
    public class ScriptInfo {
        private string m_Name;
        private string m_InternalName;
        private string m_FilePath;
        private string m_Language;
        private double m_Version;
        private string m_Author;
        private string m_Description;
        private string[] m_Notes;
        private CompilerResults m_Results = null;
        private string[] m_Source;

        /// <summary>
        /// The script's name.
        /// </summary>
        public string ScriptName {
            get {
                return m_Name;
            }
        }

        /// <summary>
        /// The script's internal name.
        /// </summary>
        public string ScriptInternalName {
            get {
                return m_InternalName;
            }
        }

        /// <summary>
        /// The script's original file path.
        /// </summary>
        public string ScriptPath {
            get {
                return m_FilePath;
            }
        }

        /// <summary>
        /// The original .NET language the script was compiled from.
        /// </summary>
        public string ScriptLanguage {
            get {
                return m_Language;
            }
        }

        /// <summary>
        /// The script's version.
        /// </summary>
        public double ScriptVersion {
            get {
                return m_Version;
            }
        }

        /// <summary>
        /// The script's author.
        /// </summary>
        public string ScriptAuthor {
            get {
                return m_Author;
            }
        }

        /// <summary>
        /// The script's description.
        /// </summary>
        public string ScriptDescription {
            get {
                return m_Description;
            }
        }

        /// <summary>
        /// Notes made by the script's author about the script.
        /// </summary>
        public string[] ScriptNotes {
            get {
                return m_Notes;
            }
        }


        /// <summary>
        /// This information is only available when a script load
        /// is complete. Gives the program details on the script's
        /// compilation. (passed or failed, errors that occured,
        /// script output)
        /// It is null if the user just requests a script list.
        /// </summary>
        public CompilerResults CompileResults {
            get {
                return m_Results;
            }
        }

        /// <summary>
        /// This information is only available when a script load
        /// is complete. Gives the full script source as a string
        /// array to the ScriptOutput so that erroring lines can
        /// be outputted for the user.
        /// </summary>
        public string[] SourceLines {
            get {
                return m_Source;
            }
        }

        /// <summary>
        /// Creates a new ScriptInfo instance.
        /// </summary>
        /// <param name="kvpinfo">The stored data that is passed to this object.</param>
        /// <param name="results">The compile results.</param>
        /// <param name="source">The script source.</param>
        internal ScriptInfo(KeyValuePair<string, string[]> kvpinfo, CompilerResults results, string[] source) {
            m_InternalName = kvpinfo.Key;
            m_Name = kvpinfo.Value[0];
            m_FilePath = kvpinfo.Value[1];
            m_Language = kvpinfo.Value[2];
            m_Version = double.Parse(kvpinfo.Value[3]);
            m_Author = kvpinfo.Value[4];
            m_Description = kvpinfo.Value[5];
            m_Notes = kvpinfo.Value[6].Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
            m_Results = results;
            m_Source = source;
        }

        /// <summary>
        /// Creates a new ScriptInfo instance.
        /// </summary>
        /// <param name="kvpinfo">The stored data that is passed to this object.</param>
        internal ScriptInfo(KeyValuePair<string, string[]> kvpinfo)
            : this(kvpinfo, null, null) {
        }
    }

    /// <summary>
    /// Specifies how a script function has executed.
    /// </summary>
    public enum ScriptFunctionReturnType {
        /// <summary>The script function has returned a value.</summary>
        ReturnsObject,
        /// <summary>The script function has a Void return type. No value is returned.</summary>
        ReturnsVoid,
        /// <summary>The script function does not exist. No value is returned.</summary>
        DoesNotExist,
        /// <summary>The script function has encountered an exception. No value is returned.</summary>
        ExecutionException
    }
}
